using PseudoCode.Core.Analyzing;
using PseudoCode.Core.Runtime.Instances;
using PseudoCode.Core.Runtime.Operations;
using PseudoCode.Core.Runtime.Reflection;
using PseudoCode.Core.Runtime.Types;
using Type = PseudoCode.Core.Runtime.Types.Type;

namespace PseudoCode.Core.Runtime;

/// <summary>
///     Represents a whole program.
/// </summary>
public class PseudoProgram
{
    /// <summary>
    ///     Stores feedbacks during parsing and typechecking (metaoperation)
    /// </summary>
    public readonly List<Feedback> AnalyserFeedbacks = new();

    /// <summary>
    ///     The outer-most scope used in this program
    /// </summary>
    public readonly Scope GlobalScope;

    /// <summary>
    ///     Simulates a memory. (uint address, Instance instance)
    /// </summary>
    public readonly Dictionary<uint, Instance> Memory = new();

    /// <summary>
    ///     Stores files opened in program (OPENFILE)
    /// </summary>
    public readonly Dictionary<string, PseudoFileStream> OpenFiles = new();

    /// <summary>
    ///     At runtime, this stack is used mainly for evaluation of an expression, kinda like postfix operations.<br />
    ///     <example>
    ///         Load a -> [ref a]<br />
    ///         Push Immediate 1 -> [ref a, 1]<br />
    ///         Push Immediate 2 -> [ref a, 1, 2]<br />
    ///         Binary Add -> [ref a, 3] (pop 2 values, add them, and push the result back)<br />
    ///         Assign -> [] (Pops 2 values, assign 3 to ref a)
    ///     </example>
    /// </summary>
    public readonly Stack<Instance> RuntimeStack = new();

    /// <summary>
    ///     After opcode generation, the program undergoes type check (meta-operate) to ensure types are right.<br />
    ///     <example>
    ///         Load a -> [Type INTEGER]<br />
    ///         Push Immediate 1 -> [Type INTEGER, Type INTEGER]<br />
    ///         Push Immediate 2 -> [Type INTEGER, Type INTEGER, Type INTEGER]<br />
    ///         Binary Add -> [Type INTEGER, Type INTEGER] (pop 2 types, check return type, push return type)<br />
    ///         Assign -> [] (Pops 2 values, check if value is assignable with/without implicit casting)
    ///     </example>
    /// </summary>
    public readonly Stack<Definition> TypeCheckStack = new();

    /// <summary>
    ///     Front address uninitialized
    /// </summary>
    public uint CurrentInstanceAddress;

    /// <summary>
    ///     If true, the values being output will specify its type and members
    /// </summary>
    public bool DebugRepresentation;

    public PseudoProgram()
    {
        GlobalScope = new Scope(null, this)
        {
            AllowStatements = true
        };
        AddPrimitiveTypes();
        AddBuiltinFunctions();
    }

    /// <summary>
    ///     Prints opcodes generated by compiler
    /// </summary>
    public bool DisplayOperationsAfterCompiled { get; set; }

    /// <summary>
    ///     Prints the operation before running
    /// </summary>
    public bool DisplayOperationsAtRuntime { get; set; }

    /// <summary>
    ///     Prohibits undeclared variables if false, disabling PLACEHOLDER type
    /// </summary>
    public bool AllowUndeclaredVariables { get; set; }


    public Definition FindDefinition(uint id)
    {
        return GlobalScope.FindDefinition(id);
    }

    public uint AllocateId(Instance i)
    {
        i.InstanceAddress = CurrentInstanceAddress++;
        i.Program = this;
        Memory.Add(i.InstanceAddress, i);
        return i.InstanceAddress;
    }

    public uint AllocateId(Func<Instance> generator)
    {
        return AllocateId(generator());
    }

    public IEnumerable<uint> Allocate(int length, Func<Instance> generator)
    {
        var startAddress = CurrentInstanceAddress;
        for (var i = 0; i < length; i++) yield return AllocateId(generator);
    }

    public void SetMemory(Range segment, Func<Instance> value)
    {
        for (var i = (uint)segment.Start; i < segment.End; i++)
        {
            Memory[i] = value();
            Memory[i].InstanceAddress = i;
            Memory[i].Program = this;
        }
    }

    public void ReleaseMemory(Range segment)
    {
        for (var i = (uint)segment.Start; i < segment.End; i++)
            Memory.Remove(i);
    }

    public void AddPrimitiveTypes()
    {
        GlobalScope.AddType(new BooleanType(GlobalScope, this));
        GlobalScope.AddType(new IntegerType(GlobalScope, this));
        GlobalScope.AddType(new RealType(GlobalScope, this));
        GlobalScope.AddType(new StringType(GlobalScope, this));
        GlobalScope.AddType(new CharacterType(GlobalScope, this));
        GlobalScope.AddType(new DateType(GlobalScope, this));
        GlobalScope.AddType(new NullType(GlobalScope, this));
        GlobalScope.AddType(new PlaceholderType(GlobalScope, this));
        GlobalScope.AddType(new ModuleType(GlobalScope, this));
        GlobalScope.AddType(new AnyType(GlobalScope, this));
        Instance.Null = GlobalScope.FindDefinition(Type.NullId).Type.Instance(scope: GlobalScope);
    }

    public void AddBuiltinFunctions()
    {
        FunctionBinder.AddBuiltinFunctionOperations(typeof(BuiltinFunctions), GlobalScope, this);
    }

    public void PrintAnalyzerFeedbacks(TextWriter textWriter)
    {
        foreach (var feedback in AnalyserFeedbacks.OrderBy(f => f.Severity)) textWriter.WriteLine(feedback);
    }
}